﻿module GameStateUpdate

open System
open Microsoft.FSharp.Core.LanguagePrimitives
open Microsoft.Xna.Framework

open WindowsGameInput

open Measure
open DataTypes
open Coroutines
open Utilities
open GameState
open TerrainFunctions
open Variable




let private UpdateBS(inBS:array<BoundingSphere>, transformation:Matrix<1>) =
    [|
        for bs in inBS do
            yield bs.Transform(transformation.GetNative())
    |]



let InitializeState (playerBS:array<BoundingSphere>, enemiesBS:array<BoundingSphere>, bulletBS:array<BoundingSphere>, difficult:int) = 

    currentState <- InitialState()
    currentState.playerBS.Value <- playerBS
    currentState.enemiesBS.Value <- enemiesBS
    currentState.bulletBS.Value <- bulletBS
    currentState.difficult.Value <- difficult



let InitializeTerrain (terrainData:float32<m>[,], cellSpacingIn:float32<m>) = 

    terrain.vertexData.Value <- InitializeTerrainVertexData terrainData cellSpacingIn
    terrain.cellData.Value <- InitializeTerrainCellData terrain.vertexData

    terrain.endZ.Value <- terrain.vertexData.Value.[0].[0].position.Z
    let lastRow = terrain.vertexData.Value.Length-1
    let lastCol = terrain.vertexData.Value.[0].Length-1
    terrain.startZ.Value <- terrain.vertexData.Value.[lastRow].[lastCol].position.Z
    terrain.minX.Value <- terrain.vertexData.Value.[0].[0].position.X
    terrain.maxX.Value <- terrain.vertexData.Value.[0].[lastCol].position.X

    terrain.spawnZTriggers := InitializeTerrainSpawnTriggers currentState.difficult.Value terrain.startZ.Value terrain.endZ.Value
    terrain.cellSpacing.Value <- cellSpacingIn



let private Collision(boundingSpheresA:seq<BoundingSphere>, boundingSpheresB:seq<BoundingSphere>) =

    use iterA = boundingSpheresA.GetEnumerator()
    use iterB = boundingSpheresB.GetEnumerator()
    let noCollision = ref true

    while iterA.MoveNext() && noCollision.Value do
        while iterB.MoveNext() && noCollision.Value do
            if iterA.Current.Intersects(iterB.Current) then
                noCollision.Value <- false

    not noCollision.Value



let private UpdateSelfBullets (self:MovableGameEntity) (targets:list<Enemy>) (dT:float32<s>) (isPlayer:bool) =
    [
        for b in !self.bullets do
            if b.coveredDistance < self.maxFireDistance then
                if IsOverTerrain terrain b.position then
                    let positionOffset = b.direction * self.fireSpeed * dT
                    let noCollision = ref true
                    for target in targets do
                        if Collision(!b.boundingSpheres, !target.data.boundingSpheres) then
                            noCollision.Value <- false
                            if isPlayer then
                                let currentScore = immediate_read(currentState.score)
                                currentState.score := currentScore + 100 + 20 * currentState.difficult.Value
                            else
                                let currentScore = immediate_read(currentState.score)
                                currentState.score := currentScore - 60 + 10 * currentState.difficult.Value
                            if !target.data.health >= self.bulletsDamage then
                                target.data.health := !target.data.health - self.bulletsDamage
                            else
                                target.data.health := 0
                    if noCollision.Value then
                        yield
                            {
                                b with
                                    position = b.position + positionOffset
                                    coveredDistance = b.coveredDistance + positionOffset.Length()
                                    boundingSpheres = Variable(fun () -> UpdateBS(currentState.bulletBS.Value,
                                                                            Matrix<1>.CreateTranslation(b.position)))
                            }
    ]



let private UpdateBullets (dT:float32<s>) = 
    currentState.player.bullets := UpdateSelfBullets currentState.player currentState.enemies.Value dT true

    for enemy in !currentState.enemies do
        enemy.data.bullets := UpdateSelfBullets enemy.data [{enemy with data = currentState.player}] dT false



let private UpdateCamera (dT:float32<s>) = 

    let newCameraPosition = !currentState.player.position + Vector3<m>(0.0f<m>, currentState.camera.playerYOffset, currentState.camera.playerZOffset)
    let terrainCol = ref 0

    let newCameraPosition =
        if IsTerrainCellInRange terrain newCameraPosition currentState.camera.terrainMinCellCol currentState.camera.terrainMaxCellCol terrainCol then
            newCameraPosition
        else
            Vector3<m>((!currentState.camera.position).X, newCameraPosition.Y, newCameraPosition.Z)


    currentState.camera.position := newCameraPosition
    currentState.camera.viewMatrix := Matrix<m>.BuildViewMatrix(currentState.camera.right, currentState.camera.up,
                                                                    currentState.camera.forward, newCameraPosition)



let private UpdateMovableEntity (entity:MovableGameEntity ref) (moveForward:bool) (moveBackward:bool)
                                (rotateLeft:bool) (rotateRight:bool) (isPlayer:bool) (dT:float32<s>) = 

    let mutable newAngle = !entity.Value.yAngle
    let mutable newSpeedOffset = 0.0f<m/s>

    if moveForward then
        newSpeedOffset <- newSpeedOffset + entity.Value.acceleration * dT
    if moveBackward then
        newSpeedOffset <- newSpeedOffset - entity.Value.acceleration * dT

    if rotateLeft then
        newAngle <- newAngle + (entity.Value.stationaryRotationSpeed + entity.Value.rotationSpeed * !entity.Value.speed) * dT
    if rotateRight then
        newAngle <- newAngle - (entity.Value.stationaryRotationSpeed + entity.Value.rotationSpeed * !entity.Value.speed) * dT

    let newSpeed =
        if newSpeedOffset <> 0.0f<m/s> then
            let newSpeed = !entity.Value.speed + newSpeedOffset
            newSpeed - entity.Value.dinamicDrag * newSpeed * dT
        else
            !entity.Value.speed - entity.Value.staticDrag * !entity.Value.speed * dT

    entity.Value.yAngle := newAngle
    entity.Value.speed := newSpeed

    let yRot = Matrix<1>.CreateRotationY(newAngle)

    let newPosition = !entity.Value.position + !entity.Value.forward * newSpeed * dT

    let terrainNormal = ref Vector3<1>.UnitY
    let terrainPos = ref Vector3<m>.Zero
    let terrainPatchIndex = ref 0

    let mutable noCollision = true
    let mutable newBoundingSphere = [||]
    let mutable translationMat = !entity.Value.translationMat

    if TerrainCollision terrain newPosition terrainPos terrainNormal terrainPatchIndex then
        noCollision <- false
    else
        translationMat <- Matrix<1>.CreateTranslation(newPosition)
        if isPlayer then
            newBoundingSphere <- UpdateBS(currentState.playerBS.Value, translationMat)
        else
            newBoundingSphere <- UpdateBS(currentState.enemiesBS.Value, translationMat)

        for e in !currentState.enemies do
            if e.data <> entity.Value && noCollision then 
                if Collision(!e.data.boundingSpheres, newBoundingSphere) then
                    noCollision <- false
        if not isPlayer then
            if Collision(!currentState.player.boundingSpheres, newBoundingSphere) then
                noCollision <- false

    if noCollision then
        let newPosition = Vector3<m>(newPosition.X, terrainPos.Value.Y + entity.Value.heightOffset, newPosition.Z)
        entity.Value.translationMat := translationMat
        entity.Value.boundingSpheres := newBoundingSphere
        entity.Value.position := newPosition
        if isPlayer then
            terrain.currentPatchIndex := terrainPatchIndex.Value
    else
        entity.Value.translationMat := !entity.Value.translationMat
        entity.Value.boundingSpheres := !entity.Value.boundingSpheres
        entity.Value.position := !entity.Value.position
        if isPlayer then
            terrain.currentPatchIndex := !terrain.currentPatchIndex

    let terrainForward = Vector3<1>.Cross(terrainNormal.Value, Vector3<1>.UnitX).Normalized()
    let terrainRight = Vector3<1>.Cross(terrainForward, terrainNormal.Value).Normalized()
    let terrainRot = Matrix<1>.CreateFromAxis(terrainRight, terrainNormal.Value, terrainForward)

    let globalRot = yRot*terrainRot
    entity.Value.forward := Vector3<1>.Transform(Vector3<1>.UnitZInverse, globalRot)
    entity.Value.rotationMat := globalRot




let private UpdatePlayer (dT:float32<s>) = 

    let keyboardState = InputHandler.GetState()

    UpdateMovableEntity (ref currentState.player) keyboardState.playerMoveForward keyboardState.playerMoveBackward
                        keyboardState.playerRotateLeft keyboardState.playerRotateRight true dT

    currentState.player.health := !currentState.player.health




let Shoots (self:MovableGameEntity)=
    co{
        SoundsHandler.PlaySound(SoundsHandler.SoundType.FIRE);
        let upVector = Vector3<1>.Transform(Vector3<1>.UnitY, immediate_read(self.rotationMat))
        let newPos = immediate_read(self.position) + upVector*1.4f<m>
        self.bullets :=
            { 
                position = newPos
                coveredDistance = 0.0f<m>
                direction = immediate_read(self.forward)
                rotationMat = immediate_read(self.rotationMat)
                boundingSpheres = Variable(fun () -> UpdateBS(currentState.bulletBS.Value, Matrix<1>.CreateTranslation(newPos)))
            } :: immediate_read(self.bullets)
        do! wait (self.fireCoolDown |> float)
    }



let ShootPlayer (enemy:Enemy) =
    co{
        do! yield_
        enemy.shooting := false
        enemy.rotateLeft := false
        enemy.rotateRight := false

        enemy.moveForward := false
        return! Shoots(enemy.data)
    }



let AimPlayer (enemy:Enemy) =
    co{
        do! yield_
        enemy.shooting := false
        enemy.moveForward := false

        let targetDistance = immediate_read(currentState.player.position) - immediate_read(enemy.data.position)
        let targetDir = targetDistance.Normalized()
        // Transforms toPlayerDir in enemy-forward space
        let invMat = Matrix<1>.Transpose(immediate_read(enemy.data.rotationMat))
        let toPlayerDir = Vector3<1>.Transform(targetDir, invMat)

        if toPlayerDir.X > 0.0f then
            enemy.rotateLeft := false
            enemy.rotateRight := true
        else
            enemy.rotateLeft := true
            enemy.rotateRight := false
    }



let AimPlayerWithDelay (enemy:Enemy) =
    co{
        do! yield_
        enemy.moveForward := false
        do! wait(Rand.NextFloat * 0.03f |> float)
        do! AimPlayer enemy
    }



let FollowPlayer (enemy:Enemy) =
    co{
        do! yield_
        do! wait(Rand.NextFloat * 0.02f |> float)
        enemy.shooting := false
        enemy.rotateLeft := false
        enemy.rotateRight := false

        enemy.moveForward := true
    }



let EnemyAI (enemy:Enemy) =

    let PlayerNear =
        co{
            do! yield_
            let distance = immediate_read(currentState.player.position) - immediate_read(enemy.data.position)
            let sqrDistanceMod = distance.LengthSquared()
            return sqrDistanceMod < 2100.0f<m^2>
        }

    let PlayerFar =
        co{
            do! yield_
            let distance = immediate_read(currentState.player.position) - immediate_read(enemy.data.position)
            let sqrDistanceMod = distance.LengthSquared()
            return sqrDistanceMod >= 2100.0f<m^2>
        }

    let PlayerAimed =
        co{
            do! yield_
            let distance = immediate_read(currentState.player.position) - immediate_read(enemy.data.position)
            let dir = Vector3<m>(distance.X, 0.0f<m>, distance.Z).Normalized()
            let forward = Vector3<1>(immediate_read(enemy.data.forward).X, 0.0f, immediate_read(enemy.data.forward).Z).Normalized()
            return Vector3<m>.Dot(immediate_read(enemy.data.forward), dir) >= 0.996f
        }

    let PlayerNotAimed =
        co{
            do! yield_
            let distance = immediate_read(currentState.player.position) - immediate_read(enemy.data.position)
            let dir = Vector3<m>(distance.X, 0.0f<m>, distance.Z).Normalized()
            let forward = Vector3<1>(immediate_read(enemy.data.forward).X, 0.0f, immediate_read(enemy.data.forward).Z).Normalized()
            return Vector3<m>.Dot(immediate_read(enemy.data.forward), dir) < 0.996f
        }


    repeat_ ((PlayerNear => ((PlayerAimed => ShootPlayer enemy) .||> (PlayerNotAimed => AimPlayer enemy))) .||>
            (PlayerFar => (FollowPlayer enemy .||> AimPlayerWithDelay enemy)))





let FastEnemiesCollision(currentPos:Vector3<m>, enemies:list<Enemy>) =

    let mutable noCollision = true

    for e in enemies do
        if noCollision && Vector3<m>.DistanceSquared(currentPos, !e.data.position) < 20.0f<m^2> then
            noCollision <- false

    not noCollision




let SpawnSingleEnemy (playerPos:Vector3<m>, enemies:list<Enemy>) =

    let newX = 10.0f<m> + terrain.minX.Value + Rand.NextFloat * (terrain.maxX.Value - 20.0f<m>)
    let mutable triedPos = Vector3<m>(newX, 100000.0f<m>, playerPos.Z-120.0f<m>)

    let newPos = ref Vector3<m>.Zero
    while TerrainCollision terrain triedPos newPos (ref Vector3<1>.UnitY) (ref 0)
            || abs(newPos.Value.Y - playerPos.Y) > 3.0f<m> || FastEnemiesCollision(newPos.Value, enemies) do
        let newX = 10.0f<m> + terrain.minX.Value + Rand.NextFloat * (terrain.maxX.Value - 20.0f<m>)
        triedPos <- Vector3<m>(newX, 100000.0f<m>, playerPos.Z-120.0f<m>)

    let newYAngle = Math.PI |> float32 |> Float32WithMeasure<rad>
    let newTranslation = Matrix<1>.CreateTranslation(newPos.Value)
    let newEnemyData =
        {
            defaultEnemyData() with
                position = variable(newPos.Value)
                yAngle = variable(newYAngle)
                translationMat = variable(newTranslation)
                rotationMat = variable(Matrix<rad>.CreateRotationY(newYAngle))
                boundingSpheres = Variable(fun () -> UpdateBS(currentState.enemiesBS.Value, newTranslation))
        }
    let newEnemy =
        match currentState.difficult.Value with
        | 0     ->  {
                        data = newEnemyData
                        shooting = variable(false)
                        moveBackward = variable(false)
                        moveForward = variable(false)
                        rotateLeft = variable(false)
                        rotateRight = variable(false)
                        aI = co{ return () } 
                    }
        | 1 | _ ->  {
                        data =
                            {
                                newEnemyData with
                                    acceleration = newEnemyData.acceleration * 1.1f;
                                    fireCoolDown = newEnemyData.fireCoolDown * 0.9f;
                                    maxFireDistance = newEnemyData.maxFireDistance * 1.1f;
                                    fireSpeed = newEnemyData.fireSpeed * 1.1f;
                                    stationaryRotationSpeed = newEnemyData.stationaryRotationSpeed * 1.2f
                                    bulletsDamage = newEnemyData.bulletsDamage + 10
                                    health = variable(!newEnemyData.health + 300)
                            }
                        shooting = variable(false)
                        moveBackward = variable(false)
                        moveForward = variable(false)
                        rotateLeft = variable(false)
                        rotateRight = variable(false)
                        aI = co{ return () }
                    }
    newEnemy.aI <- EnemyAI newEnemy
    newEnemy



let private UpdateEnemies (dT:float32<s>) = 
    currentState.enemies :=
        [
            for e in !currentState.enemies do
                if !e.data.health > 0  then
                    if e.data.position.Value.Z - currentState.player.position.Value.Z < 80.0f<m> then
                        UpdateMovableEntity (ref e.data) !e.moveForward !e.moveBackward !e.rotateLeft !e.rotateRight false dT
                        e.shooting := !e.shooting
                        e.moveBackward := !e.moveBackward
                        e.moveForward := !e.moveForward
                        e.rotateLeft := !e.rotateLeft
                        e.rotateRight := !e.rotateRight
                        e.aI <- co_step(e.aI())
                        yield e
                    elif Rand.NextFloat * 2.0f > 1.0f then // ~50% chance of respawn
                        yield SpawnSingleEnemy(!currentState.player.position, !currentState.enemies)
                else
                    SoundsHandler.PlaySound(SoundsHandler.SoundType.EXPLOSION);
        ]



let UpdateState (dT:float32<s>) = 

    assert(dT > 0.0f<s>)
    assert(terrain.vertexData.Value <> [||])
    assert(terrain.cellData.Value <> [||])

    currentState.score := !currentState.score
    currentState.gameWon := !currentState.gameWon
    currentState.gameLost := !currentState.gameLost

    UpdateCamera(dT)
    if(!terrain.spawnZTriggers <> []) then
        terrain.spawnZTriggers := !terrain.spawnZTriggers

    let spawns = !terrain.spawnZTriggers
    UpdatePlayer(dT)
    UpdateBullets(dT)
    UpdateEnemies(dT)

    if immediate_read(currentState.player.health) <= 0 then
        currentState.gameLost := true
        let currentScore = immediate_read(currentState.score)
        currentState.score := currentScore - ((abs(currentScore) |> float32) * 0.5f |> int)
    else if immediate_read(currentState.player.position).Z <= -4800.0f<m> then
        currentState.gameWon := true
        let currentScore = immediate_read(currentState.score)
        currentState.score := currentScore + ((abs(currentScore) |> float32) * 0.4f |> int)




let private (!) = immediate_read
let private (:=) = immediate_write


let private main =

    assert(terrain.vertexData.Value <> [||])

    let rec PlayerShoot() = 
        co{
            do! yield_
            if InputHandler.GetState().playerShoot then
                return! Shoots(currentState.player)
        }


    let rec SpawnEnemies() = 
        co{
            do! yield_
            let playerPos = !currentState.player.position
            match !terrain.spawnZTriggers with
            | [] -> do! wait (1.0)
            | x :: xs -> if playerPos.Z < x then
                            terrain.spawnZTriggers := xs
                            match currentState.difficult.Value with
                            | 0     ->  currentState.enemies := SpawnSingleEnemy(playerPos, !currentState.enemies) :: !currentState.enemies
                                        currentState.enemies := SpawnSingleEnemy(playerPos, !currentState.enemies) :: !currentState.enemies
                            | 1 | _ ->  currentState.enemies := SpawnSingleEnemy(playerPos, !currentState.enemies) :: !currentState.enemies
                                        currentState.enemies := SpawnSingleEnemy(playerPos, !currentState.enemies) :: !currentState.enemies
                                        currentState.enemies := SpawnSingleEnemy(playerPos, !currentState.enemies) :: !currentState.enemies
            do! wait (0.5)
        }

    repeat_(PlayerShoot() .||> SpawnEnemies()) |> ref



let UpdateScript() =
    main.Value <- update_ai(main.Value)